/**
 Copyright (c) 2014-present, Facebook, Inc.
 All rights reserved.
 
 This source code is licensed under the BSD-style license found in the
 LICENSE file in the root directory of this source tree. An additional grant
 of patent rights can be found in the PATENTS file in the same directory.
 */

#import "FBShimmeringLayer.h"

#import <QuartzCore/CAAnimation.h>
#import <QuartzCore/CAGradientLayer.h>
#import <QuartzCore/CATransaction.h>

#import <UIKit/UIGeometry.h>
#import <UIKit/UIColor.h>

#if !__has_feature(objc_arc)
#error This file must be compiled with ARC. Convert your project to ARC or specify the -fobjc-arc flag.
#endif

#if TARGET_IPHONE_SIMULATOR
UIKIT_EXTERN float UIAnimationDragCoefficient(void); // UIKit private drag coeffient, use judiciously
#endif

static CGFloat FBShimmeringLayerDragCoefficient(void)
{
#if TARGET_IPHONE_SIMULATOR
    return UIAnimationDragCoefficient();
#else
    return 1.0;
#endif
}

static void FBShimmeringLayerAnimationApplyDragCoefficient(CAAnimation *animation)
{
    CGFloat k = FBShimmeringLayerDragCoefficient();
    
    if (k != 0 && k != 1) {
        animation.speed = 1 / k;
    }
}

// animations keys
static NSString *const kFBShimmerSlideAnimationKey = @"slide";
static NSString *const kFBFadeAnimationKey = @"fade";
static NSString *const kFBEndFadeAnimationKey = @"fade-end";

static CABasicAnimation *fade_animation(CALayer *layer, CGFloat opacity, CFTimeInterval duration)
{
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
    animation.fromValue = @([(layer.presentationLayer ?: layer) opacity]);
    animation.toValue = @(opacity);
    animation.fillMode = kCAFillModeBoth;
    animation.removedOnCompletion = NO;
    animation.duration = duration;
    FBShimmeringLayerAnimationApplyDragCoefficient(animation);
    return animation;
}

static CABasicAnimation *shimmer_slide_animation(CFTimeInterval duration, FBShimmerDirection direction)
{
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
    animation.toValue = [NSValue valueWithCGPoint:CGPointZero];
    animation.duration = duration;
    animation.repeatCount = HUGE_VALF;
    FBShimmeringLayerAnimationApplyDragCoefficient(animation);
    if (direction == FBShimmerDirectionLeft ||
        direction == FBShimmerDirectionUp) {
        animation.speed = -fabsf(animation.speed);
    }
    return animation;
}

// take a shimmer slide animation and turns into repeating
static CAAnimation *shimmer_slide_repeat(CAAnimation *a, CFTimeInterval duration, FBShimmerDirection direction)
{
    CAAnimation *anim = [a copy];
    anim.repeatCount = HUGE_VALF;
    anim.duration = duration;
    anim.speed = (direction == FBShimmerDirectionRight || direction == FBShimmerDirectionDown) ? fabsf(anim.speed) : -fabsf(anim.speed);
    return anim;
}

// take a shimmer slide animation and turns into finish
static CAAnimation *shimmer_slide_finish(CAAnimation *a)
{
    CAAnimation *anim = [a copy];
    anim.repeatCount = 0;
    return anim;
}

@interface FBShimmeringMaskLayer : CAGradientLayer
@property (readonly, nonatomic) CALayer *fadeLayer;
@end

@implementation FBShimmeringMaskLayer

- (instancetype)init
{
    self = [super init];
    if (nil != self) {
        _fadeLayer = [[CALayer alloc] init];
        _fadeLayer.backgroundColor = [UIColor whiteColor].CGColor;
        [self addSublayer:_fadeLayer];
    }
    return self;
}

- (void)layoutSublayers
{
    [super layoutSublayers];
    CGRect r = self.bounds;
    _fadeLayer.bounds = r;
    _fadeLayer.position = CGPointMake(CGRectGetMidX(r), CGRectGetMidY(r));
}

@end

@interface FBShimmeringLayer ()
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000
// iOS 10 SDK has CALayerDelegate and CAAnimationDelegate as proper protocols.
<CALayerDelegate, CAAnimationDelegate>
#endif

@property (strong, nonatomic) FBShimmeringMaskLayer *maskLayer;
@end

@implementation FBShimmeringLayer
{
    CALayer *_contentLayer;
    FBShimmeringMaskLayer *_maskLayer;
}

#pragma mark - Lifecycle

@synthesize shimmering = _shimmering;
@synthesize shimmeringPauseDuration = _shimmeringPauseDuration;
@synthesize shimmeringAnimationOpacity = _shimmeringAnimationOpacity;
@synthesize shimmeringOpacity = _shimmeringOpacity;
@synthesize shimmeringSpeed = _shimmeringSpeed;
@synthesize shimmeringHighlightLength = _shimmeringHighlightLength;
@synthesize shimmeringDirection = _shimmeringDirection;
@synthesize shimmeringFadeTime = _shimmeringFadeTime;
@synthesize shimmeringBeginFadeDuration = _shimmeringBeginFadeDuration;
@synthesize shimmeringEndFadeDuration = _shimmeringEndFadeDuration;
@synthesize shimmeringBeginTime = _shimmeringBeginTime;
@dynamic shimmeringHighlightWidth;

- (instancetype)init
{
    self = [super init];
    if (nil != self) {
        // default configuration
        _shimmeringPauseDuration = 0.4;
        _shimmeringSpeed = 230.0;
        _shimmeringHighlightLength = 1.0;
        _shimmeringAnimationOpacity = 0.5;
        _shimmeringOpacity = 1.0;
        _shimmeringDirection = FBShimmerDirectionRight;
        _shimmeringBeginFadeDuration = 0.1;
        _shimmeringEndFadeDuration = 0.3;
        _shimmeringBeginTime = FBShimmerDefaultBeginTime;
    }
    return self;
}

#pragma mark - Properties

- (void)setContentLayer:(CALayer *)contentLayer
{
    // reset mask
    self.maskLayer = nil;
    
    // note content layer and add for display
    _contentLayer = contentLayer;
    self.sublayers = contentLayer ? @[contentLayer] : nil;
    
    // update shimmering animation
    [self _updateShimmering];
}

- (void)setShimmering:(BOOL)shimmering
{
    if (shimmering != _shimmering) {
        _shimmering = shimmering;
        [self _updateShimmering];
    }
}

- (void)setShimmeringSpeed:(CGFloat)speed
{
    if (speed != _shimmeringSpeed) {
        _shimmeringSpeed = speed;
        [self _updateShimmering];
    }
}

- (void)setShimmeringHighlightLength:(CGFloat)length
{
    if (length != _shimmeringHighlightLength) {
        _shimmeringHighlightLength = length;
        [self _updateShimmering];
    }
}

- (void)setShimmeringDirection:(FBShimmerDirection)direction
{
    if (direction != _shimmeringDirection) {
        _shimmeringDirection = direction;
        [self _updateShimmering];
    }
}

- (void)setShimmeringPauseDuration:(CFTimeInterval)duration
{
    if (duration != _shimmeringPauseDuration) {
        _shimmeringPauseDuration = duration;
        [self _updateShimmering];
    }
}

- (void)setShimmeringAnimationOpacity:(CGFloat)shimmeringAnimationOpacity
{
    if (shimmeringAnimationOpacity != _shimmeringAnimationOpacity) {
        _shimmeringAnimationOpacity = shimmeringAnimationOpacity;
        [self _updateMaskColors];
    }
}

- (void)setShimmeringOpacity:(CGFloat)shimmeringOpacity
{
    if (shimmeringOpacity != _shimmeringOpacity) {
        _shimmeringOpacity = shimmeringOpacity;
        [self _updateMaskColors];
    }
}

- (void)setShimmeringBeginTime:(CFTimeInterval)beginTime
{
    if (beginTime != _shimmeringBeginTime) {
        _shimmeringBeginTime = beginTime;
        [self _updateShimmering];
    }
}

- (void)layoutSublayers
{
    [super layoutSublayers];
    CGRect r = self.bounds;
    _contentLayer.anchorPoint = CGPointMake(0.5, 0.5);
    _contentLayer.bounds = r;
    _contentLayer.position = CGPointMake(CGRectGetMidX(r), CGRectGetMidY(r));
    
    if (nil != _maskLayer) {
        [self _updateMaskLayout];
    }
}

- (void)setBounds:(CGRect)bounds
{
    CGRect oldBounds = self.bounds;
    [super setBounds:bounds];
    
    if (!CGRectEqualToRect(oldBounds, bounds)) {
        [self _updateShimmering];
    }
}

#pragma mark - Internal

- (void)_clearMask
{
    if (nil == _maskLayer) {
        return;
    }
    
    BOOL disableActions = [CATransaction disableActions];
    [CATransaction setDisableActions:YES];
    
    self.maskLayer = nil;
    _contentLayer.mask = nil;
    
    [CATransaction setDisableActions:disableActions];
}

- (void)_createMaskIfNeeded
{
    if (_shimmering && !_maskLayer) {
        _maskLayer = [FBShimmeringMaskLayer layer];
        _maskLayer.delegate = self;
        _contentLayer.mask = _maskLayer;
        [self _updateMaskColors];
        [self _updateMaskLayout];
    }
}

- (void)_updateMaskColors
{
    if (nil == _maskLayer) {
        return;
    }
    
    // We create a gradient to be used as a mask.
    // In a mask, the colors do not matter, it's the alpha that decides the degree of masking.
    UIColor *maskedColor = [UIColor colorWithWhite:1.0 alpha:_shimmeringOpacity];
    UIColor *unmaskedColor = [UIColor colorWithWhite:1.0 alpha:_shimmeringAnimationOpacity];
    
    // Create a gradient from masked to unmasked to masked.
    _maskLayer.colors = @[(__bridge id)maskedColor.CGColor, (__bridge id)unmaskedColor.CGColor, (__bridge id)maskedColor.CGColor];
}

- (void)_updateMaskLayout
{
    // Everything outside the mask layer is hidden, so we need to create a mask long enough for the shimmered layer to be always covered by the mask.
    CGFloat length = 0.0f;
    if (_shimmeringDirection == FBShimmerDirectionDown ||
        _shimmeringDirection == FBShimmerDirectionUp) {
        length = CGRectGetHeight(_contentLayer.bounds);
    } else {
        length = CGRectGetWidth(_contentLayer.bounds);
    }
    if (0 == length) {
        return;
    }
    
    // extra distance for the gradient to travel during the pause.
    CGFloat extraDistance = length + _shimmeringSpeed * _shimmeringPauseDuration;
    
    // compute how far the shimmering goes
    CGFloat fullShimmerLength = length * 3.0f + extraDistance;
    CGFloat travelDistance = length * 2.0f + extraDistance;
    
    // position the gradient for the desired width
    CGFloat highlightOutsideLength = (1.0 - _shimmeringHighlightLength) / 2.0;
    _maskLayer.locations = @[@(highlightOutsideLength),
                             @(0.5),
                             @(1.0 - highlightOutsideLength)];
    
    CGFloat startPoint = (length + extraDistance) / fullShimmerLength;
    CGFloat endPoint = travelDistance / fullShimmerLength;
    
    // position for the start of the animation
    _maskLayer.anchorPoint = CGPointZero;
    if (_shimmeringDirection == FBShimmerDirectionDown ||
        _shimmeringDirection == FBShimmerDirectionUp) {
        _maskLayer.startPoint = CGPointMake(0.0, startPoint);
        _maskLayer.endPoint = CGPointMake(0.0, endPoint);
        _maskLayer.position = CGPointMake(0.0, -travelDistance);
        _maskLayer.bounds = CGRectMake(0.0, 0.0, CGRectGetWidth(_contentLayer.bounds), fullShimmerLength);
    } else {
        _maskLayer.startPoint = CGPointMake(startPoint, 0.0);
        _maskLayer.endPoint = CGPointMake(endPoint, 0.0);
        _maskLayer.position = CGPointMake(-travelDistance, 0.0);
        _maskLayer.bounds = CGRectMake(0.0, 0.0, fullShimmerLength, CGRectGetHeight(_contentLayer.bounds));
    }
}

- (void)_updateShimmering
{
    // create mask if needed
    [self _createMaskIfNeeded];
    
    // if not shimmering and no mask, noop
    if (!_shimmering && !_maskLayer) {
        return;
    }
    
    // ensure layout
    [self layoutIfNeeded];
    
    BOOL disableActions = [CATransaction disableActions];
    if (!_shimmering) {
        if (disableActions) {
            // simply remove mask
            [self _clearMask];
        } else {
            // end slide
            CFTimeInterval slideEndTime = 0;
            
            CAAnimation *slideAnimation = [_maskLayer animationForKey:kFBShimmerSlideAnimationKey];
            if (slideAnimation != nil) {
                
                // determine total time sliding
                CFTimeInterval now = CACurrentMediaTime();
                CFTimeInterval slideTotalDuration = now - slideAnimation.beginTime;
                
                // determine time offset into current slide
                CFTimeInterval slideTimeOffset = fmod(slideTotalDuration, slideAnimation.duration);
                
                // transition to non-repeating slide
                CAAnimation *finishAnimation = shimmer_slide_finish(slideAnimation);
                
                // adjust begin time to now - offset
                finishAnimation.beginTime = now - slideTimeOffset;
                
                // note slide end time and begin
                slideEndTime = finishAnimation.beginTime + slideAnimation.duration;
                [_maskLayer addAnimation:finishAnimation forKey:kFBShimmerSlideAnimationKey];
            }
            
            // fade in text at slideEndTime
            CABasicAnimation *fadeInAnimation = fade_animation(_maskLayer.fadeLayer, 1.0, _shimmeringEndFadeDuration);
            fadeInAnimation.delegate = self;
            [fadeInAnimation setValue:@YES forKey:kFBEndFadeAnimationKey];
            fadeInAnimation.beginTime = slideEndTime;
            [_maskLayer.fadeLayer addAnimation:fadeInAnimation forKey:kFBFadeAnimationKey];
            
            // expose end time for synchronization
            _shimmeringFadeTime = slideEndTime;
        }
    } else {
        // fade out text, optionally animated
        CABasicAnimation *fadeOutAnimation = nil;
        if (_shimmeringBeginFadeDuration > 0.0 && !disableActions) {
            fadeOutAnimation = fade_animation(_maskLayer.fadeLayer, 0.0, _shimmeringBeginFadeDuration);
            [_maskLayer.fadeLayer addAnimation:fadeOutAnimation forKey:kFBFadeAnimationKey];
        } else {
            BOOL innerDisableActions = [CATransaction disableActions];
            [CATransaction setDisableActions:YES];
            
            _maskLayer.fadeLayer.opacity = 0.0;
            [_maskLayer.fadeLayer removeAllAnimations];
            
            [CATransaction setDisableActions:innerDisableActions];
        }
        
        // begin slide animation
        CAAnimation *slideAnimation = [_maskLayer animationForKey:kFBShimmerSlideAnimationKey];
        
        // compute shimmer duration
        CGFloat length = 0.0f;
        if (_shimmeringDirection == FBShimmerDirectionDown ||
            _shimmeringDirection == FBShimmerDirectionUp) {
            length = CGRectGetHeight(_contentLayer.bounds);
        } else {
            length = CGRectGetWidth(_contentLayer.bounds);
        }
        CFTimeInterval animationDuration = (length / _shimmeringSpeed) + _shimmeringPauseDuration;
        
        if (slideAnimation != nil) {
            // ensure existing slide animation repeats
            [_maskLayer addAnimation:shimmer_slide_repeat(slideAnimation, animationDuration, _shimmeringDirection) forKey:kFBShimmerSlideAnimationKey];
        } else {
            // add slide animation
            slideAnimation = shimmer_slide_animation(animationDuration, _shimmeringDirection);
            slideAnimation.fillMode = kCAFillModeForwards;
            slideAnimation.removedOnCompletion = NO;
            if (_shimmeringBeginTime == FBShimmerDefaultBeginTime) {
                _shimmeringBeginTime = CACurrentMediaTime() + fadeOutAnimation.duration;
            }
            slideAnimation.beginTime = _shimmeringBeginTime;
            
            [_maskLayer addAnimation:slideAnimation forKey:kFBShimmerSlideAnimationKey];
        }
    }
}

#pragma mark - CALayerDelegate

- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
    // no associated actions
    return (id)kCFNull;
}

#pragma mark - CAAnimationDelegate

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
    if (flag && [[anim valueForKey:kFBEndFadeAnimationKey] boolValue]) {
        [_maskLayer.fadeLayer removeAnimationForKey:kFBFadeAnimationKey];
        
        [self _clearMask];
    }
}

@end
